View Javadoc
1   package edu.jiangxin.apktoolbox.convert.protobuf.supervised;
2   
3   import com.google.protobuf.DescriptorProtos;
4   import com.google.protobuf.Descriptors;
5   import com.google.protobuf.InvalidProtocolBufferException;
6   
7   import java.io.IOException;
8   import java.io.UncheckedIOException;
9   import java.nio.file.Files;
10  import java.nio.file.Path;
11  import java.util.*;
12  import java.util.stream.Stream;
13  
14  /**
15   * Cache containing protobuf descriptors used by {@link ProtoToJson} in order to decode binary protobuf messages.
16   * <p>
17   * An instance can be created using the static factory methods {@link #emptyCache()}, {@link #fromDirectory(Path)} and
18   * {@link #fromFile(Path)}.
19   * <p>
20   * Additional descriptors can be added using {@link #addDescriptor(Descriptors.Descriptor)}, {@link
21   * #addDescriptors(Path)} and {@link #addDescriptors(byte[])}. They can be received using {@link #getByTypeName(String)}
22   * and {@link #getDescriptors()}.
23   * <p>
24   * Descriptors can be obtained by applying a {@code protoc} command on the protobuf schema {@code .proto}, for example:
25   * <pre>{@code
26   * protoc --descriptor_set_out foo.desc foo.proto
27   * }</pre>
28   *
29   * @author Daniel Tischner {@literal <zabuza.dev@gmail.com>}
30   */
31  public final class DescriptorCache {
32  	private static final Descriptors.FileDescriptor[] DEPENDENCIES = new Descriptors.FileDescriptor[0];
33  
34  	/**
35  	 * Creates an instance that initially has no descriptors.
36  	 *
37  	 * @return The created instance
38  	 */
39  	public static DescriptorCache emptyCache() {
40  		return new DescriptorCache();
41  	}
42  
43  	/**
44  	 * Creates an instance from a directory containing descriptor files.
45  	 * <p>
46  	 * The directory must not contain files that are not valid descriptor files.
47  	 * <p>
48  	 * Descriptor files can be obtained by applying a {@code protoc} command on the protobuf schema {@code .proto}, for
49  	 * example:
50  	 * <pre>{@code
51  	 * protoc --descriptor_set_out foo.desc foo.proto
52  	 * }</pre>
53  	 *
54  	 * @param directory The directory containing the descriptor files, not null.
55  	 *
56  	 * @return The created instance that has all descriptors available in the given directory
57  	 *
58  	 * @throws IllegalArgumentException                If the {@code directory} is not a directory
59  	 * @throws UncheckedIOException                    If an I/O error occurs during reading the files
60  	 * @throws UncheckedInvalidProtocolBufferException If a file is not a valid descriptor file
61  	 * @throws UncheckedDescriptorValidationException  If a file contains malformed descriptors
62  	 */
63  	public static DescriptorCache fromDirectory(final Path directory) {
64  		Objects.requireNonNull(directory);
65  
66  		if (!Files.isDirectory(directory)) {
67  			throw new IllegalArgumentException("Path must be a directory: " + directory);
68  		}
69  
70  		final DescriptorCache cache = new DescriptorCache();
71  		try (final Stream<Path> walk = Files.walk(directory)) {
72  			walk.filter(Files::isRegularFile)
73  					.forEach(cache::addDescriptors);
74  		} catch (final IOException e) {
75  			throw new UncheckedIOException(e);
76  		}
77  		return cache;
78  	}
79  
80  	/**
81  	 * Creates an instance from a single descriptor file.
82  	 * <p>
83  	 * Descriptor files can be obtained by applying a {@code protoc} command on the protobuf schema {@code .proto}, for
84  	 * example:
85  	 * <pre>{@code
86  	 * protoc --descriptor_set_out foo.desc foo.proto
87  	 * }</pre>
88  	 *
89  	 * @param descriptorsFile The descriptor file, not null.
90  	 *
91  	 * @return The created instance that has all descriptors available in the given file
92  	 *
93  	 * @throws IllegalArgumentException                If the {@code descriptorsFile} is not a regular file
94  	 * @throws UncheckedIOException                    If an I/O error occurs during reading the files
95  	 * @throws UncheckedInvalidProtocolBufferException If the file is not a valid descriptor file
96  	 * @throws UncheckedDescriptorValidationException  If the file contains malformed descriptors
97  	 */
98  	public static DescriptorCache fromFile(final Path descriptorsFile) {
99  		Objects.requireNonNull(descriptorsFile);
100 
101 		if (!Files.isRegularFile(descriptorsFile)) {
102 			throw new IllegalArgumentException("Path must be a regular file: " + descriptorsFile);
103 		}
104 		final DescriptorCache cache = new DescriptorCache();
105 		cache.addDescriptors(descriptorsFile);
106 		return cache;
107 	}
108 
109 	/**
110 	 * Maps message type names to the descriptor suitable for parsing messages of that type, not null.
111 	 */
112 	private final Map<String, Descriptors.Descriptor> typeNameToDescriptor = new HashMap<>();
113 
114 	/**
115 	 * Creates a new instance with no descriptors.
116 	 */
117 	private DescriptorCache() {
118 
119 	}
120 
121 	/**
122 	 * Adds the given descriptor to the cache.
123 	 * <p>
124 	 * Overwriting any descriptor previously registered for the same message type.
125 	 *
126 	 * @param descriptor The descriptor to add, not null
127 	 *
128 	 * @return The descriptor previously associated to the message type, if any
129 	 */
130 	@SuppressWarnings({ "WeakerAccess", "UnusedReturnValue" })
131 	public Optional<Descriptors.Descriptor> addDescriptor(final Descriptors.Descriptor descriptor) {
132 		Objects.requireNonNull(descriptor);
133 		final String typeName = Objects.requireNonNull(descriptor.getName());
134 
135 		return Optional.ofNullable(typeNameToDescriptor.put(typeName, descriptor));
136 	}
137 
138 	/**
139 	 * Adds all descriptors given in a descriptor file to the cache.
140 	 * <p>
141 	 * Overwriting any descriptors previously registered for the same message types.
142 	 * <p>
143 	 * Descriptor files can be obtained by applying a {@code protoc} command on the protobuf schema {@code .proto}, for
144 	 * example:
145 	 * <pre>{@code
146 	 * protoc --descriptor_set_out foo.desc foo.proto
147 	 * }</pre>
148 	 *
149 	 * @param descriptorsFile The descriptor file, not null.
150 	 *
151 	 * @throws IllegalArgumentException                If the {@code descriptorsFile} is not a regular file
152 	 * @throws UncheckedIOException                    If an I/O error occurs during reading the file
153 	 * @throws UncheckedInvalidProtocolBufferException If the file is not a valid descriptor file
154 	 * @throws UncheckedDescriptorValidationException  If the file contains malformed descriptors
155 	 */
156 	@SuppressWarnings("WeakerAccess")
157 	public void addDescriptors(final Path descriptorsFile) {
158 		Objects.requireNonNull(descriptorsFile);
159 
160 		if (!Files.isRegularFile(descriptorsFile)) {
161 			throw new IllegalArgumentException("Path must be a regular file: " + descriptorsFile);
162 		}
163 
164 		try {
165 			addDescriptors(Files.readAllBytes(descriptorsFile));
166 		} catch (final IOException e) {
167 			throw new UncheckedIOException("While reading: " + descriptorsFile.toAbsolutePath(), e);
168 		}
169 	}
170 
171 	/**
172 	 * Adds all descriptors given as a raw descriptor set to the cache.
173 	 * <p>
174 	 * Overwriting any descriptors previously registered for the same message types.
175 	 *
176 	 * @param descriptorsRaw The raw descriptor set, not null.
177 	 *
178 	 * @throws UncheckedInvalidProtocolBufferException If the file is not a valid descriptor file
179 	 * @throws UncheckedDescriptorValidationException  If the file contains malformed descriptors
180 	 */
181 	@SuppressWarnings("WeakerAccess")
182 	public void addDescriptors(final byte[] descriptorsRaw) {
183 		Objects.requireNonNull(descriptorsRaw);
184 
185 		try {
186 			final DescriptorProtos.FileDescriptorSet descriptorSet =
187 					DescriptorProtos.FileDescriptorSet.parseFrom(descriptorsRaw);
188 			for (final DescriptorProtos.FileDescriptorProto descriptorFile : descriptorSet.getFileList()) {
189 				final Descriptors.FileDescriptor fileDescriptor =
190 						Descriptors.FileDescriptor.buildFrom(descriptorFile, DescriptorCache.DEPENDENCIES);
191 				for (final Descriptors.Descriptor descriptor : fileDescriptor.getMessageTypes()) {
192 					addDescriptor(descriptor);
193 				}
194 			}
195 		} catch (final InvalidProtocolBufferException e) {
196 			throw new UncheckedInvalidProtocolBufferException(e);
197 		} catch (final Descriptors.DescriptorValidationException e) {
198 			throw new UncheckedDescriptorValidationException(e);
199 		}
200 	}
201 
202 	/**
203 	 * Gets the descriptor registered for the given message type name, if any.
204 	 *
205 	 * @param typeName The message type name, not null
206 	 *
207 	 * @return The descriptor registered for the given message type name, if any
208 	 */
209 	public Optional<Descriptors.Descriptor> getByTypeName(final String typeName) {
210 		Objects.requireNonNull(typeName);
211 
212 		return Optional.ofNullable(typeNameToDescriptor.get(typeName));
213 	}
214 
215 	/**
216 	 * Gets all descriptors registered by this cache.
217 	 *
218 	 * @return An unmodifiable collection of all registered descriptors
219 	 */
220 	public Collection<Descriptors.Descriptor> getDescriptors() {
221 		return Collections.unmodifiableCollection(typeNameToDescriptor.values());
222 	}
223 
224 	/**
225 	 * Gets all by this cache registered mappings of message type names to their descriptors.
226 	 *
227 	 * @return An unmodifiable collection of all registered message type name to descriptor mappings
228 	 */
229 	public Collection<Map.Entry<String, Descriptors.Descriptor>> getEntries() {
230 		return Collections.unmodifiableCollection(typeNameToDescriptor.entrySet());
231 	}
232 
233 	/**
234 	 * Whether the cache has no descriptors registered.
235 	 *
236 	 * @return True if the cache has no descriptors registered, false otherwise
237 	 */
238 	public boolean isEmpty() {
239 		return typeNameToDescriptor.isEmpty();
240 	}
241 
242 	/**
243 	 * Gets how many descriptors are currently registered to the cache.
244 	 *
245 	 * @return The amount of descriptors registered to the cache
246 	 */
247 	public int size() {
248 		return typeNameToDescriptor.size();
249 	}
250 }